﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using DslModeling=global::Microsoft.VisualStudio.Modeling;
using DslDesign=global::Microsoft.VisualStudio.Modeling.Design;
using DslValidation=global::Microsoft.VisualStudio.Modeling.Validation;

namespace Salamanca.Sdml
{
    public interface IDataTableHolder
    {
        DataTable DataTable { get; }
    }

    partial class DomainModel
    {

        /// <summary>In-memory database structure implied by the current <see cref="DomainModel" />.</summary>
        public DataSet DataSet
        {
            get
            {
                LoadDataSet();
                return _DataSet;
            }
        }

        internal void LoadDataSet()
        {
            if (_DataSet==null)
            {
                DataSet ds=null;
                if (string.IsNullOrEmpty(Name))
                    ds=new DataSet();
                else
                    ds=new DataSet(Name);

                DataTable dt;
                DataColumn dc;

                // Sort the elements in reference order in order to generate tables in the correct order
                List<DslModeling.ModelElement> elements=new List<DslModeling.ModelElement>(Partition.ElementDirectory.AllElements)
                    .FindAll(
                        delegate(DslModeling.ModelElement me) {
                            // Get domain entities...
                            if (me is DomainEntity)
                                return true;

                            // ... and n-n associations.
                            Association association=me as Association;
                            if (association!=null)
                                return (!Association.IsSingleMultiplicity(association.SourceMultiplicity) && !Association.IsSingleMultiplicity(association.TargetMultiplicity));

                            return false;
                        }
                    );
                elements.Sort(
                    delegate(DslModeling.ModelElement me1, DslModeling.ModelElement me2) {
                        if (me1==me2)
                            return 0;

                        // nulls come first
                        if (me1==null)
                            return -1;
                        if (me2==null)
                            return 1;

                        // Domain Entities come first
                        DomainEntity de1=me1 as DomainEntity;
                        DomainEntity de2=me2 as DomainEntity;
                        if ((de1!=null)||(de2!=null))
                        {
                            if ((de1!=null) && (de2==null))
                                return -1;
                            if ((de1==null) && (de2!=null))
                                return 1;

                            return de1.CompareTo(de2);
                        }

                        return 0;
                    }
                );

                // Create a table for every element
                foreach (DslModeling.ModelElement element in elements)
                {
                    DomainEntity entity=element as DomainEntity;
                    if (entity!=null)
                    {
                        dt=ds.Tables.Add(entity.TableName);
                        dt.ExtendedProperties.Add(typeof(DslModeling.ModelElement), entity);
                        entity.DataTable=dt;

                        // Primary keys
                        if (entity.Parent!=null)
                        {
                            List<DataColumn> keys=new List<DataColumn>();
                            keys.AddRange(
                                CreateColumnsFromReferenceTable(
                                    dt,
                                    ds.Tables[entity.Parent.TableName],
                                    Rule.Cascade,
                                    Rule.Cascade,
                                    null
                                )
                            );

                            dt.Constraints.Add(
                                string.Concat("PK_", dt.TableName),
                                keys.ToArray(),
                                true
                            );
                            dt.ParentRelations[dt.ParentRelations.Count-1].Nested=true;
                        } else if (entity.MultipleAssociation!=null)
                        {
                            List<DataColumn> keys=new List<DataColumn>();
                            foreach (DomainEntity associated in entity.MultipleAssociation.Types)
                            {
                                keys.AddRange(
                                    CreateColumnsFromReferenceTable(
                                        dt,
                                        ds.Tables[associated.TableName],
                                        Rule.None,
                                        Rule.None,
                                        associated.Name
                                    )
                                );
                            }

                            dt.Constraints.Add(
                                string.Concat("PK_", dt.TableName),
                                keys.ToArray(),
                                true
                            );
                        } else
                        {
                            dc=dt.Columns.Add("Id", entity.IdType.DataType);
                            dc.ExtendedProperties.Add(typeof(SdmlType), entity.IdType);

                            dt.Constraints.Add(
                                string.Concat("PK_", dt.TableName),
                                dc,
                                true
                            );
                        }

                        // Foreign keys
                        List<Association> aList=new List<Association>(
                            DslModeling::DomainRoleInfo.GetElementLinks<Association>(
                                entity,
                                Association.TargetDomainRoleId
                            )
                        ).FindAll(
                            delegate(Association a) {
                                return Association.IsSingleMultiplicity(a.SourceMultiplicity);
                            }
                        );

                        foreach (Association a in aList)
                        {
                            DomainEntity referenced=a.Source;
                            string roleName=a.GetSourceRoleName();
                            Multiplicity multiplicity=a.SourceMultiplicity;

                            Rule rule=Rule.None;
                            switch (a.Form)
                            {
                            case AssociationForm.Composition:
                                rule=Rule.Cascade;
                                break;
                            case AssociationForm.Aggregation:
                                if (!Association.IsMandatoryMultiplicity(multiplicity))
                                    rule=Rule.SetNull;
                                break;
                            }

                            foreach (DataColumn column in CreateColumnsFromReferenceTable(
                                    dt,
                                    ds.Tables[a.Source.TableName],
                                    rule,
                                    rule,
                                    roleName
                                )
                            )
                            {
                                if (Association.IsMandatoryMultiplicity(multiplicity))
                                    column.AllowDBNull=false;
                            }
                            a.DataRelation=dt.ParentRelations[dt.ParentRelations.Count-1];
                        }

                        // Attributes
                        foreach (EntityAttribute attribute in entity.Attributes)
                        {
                            SdmlType st=TypesProvider.GetType(attribute);

                            dc=dt.Columns.Add(attribute.Name, st.DataType);
                            dc.AllowDBNull=attribute.AllowNull;
                            dc.MaxLength=attribute.MaxLength;
                            dc.ExtendedProperties.Add(typeof(SdmlType), st);
                            attribute.DataColumn=dc;

                            if (attribute.Unique)
                                dt.Constraints.Add(
                                    string.Concat("UN_", dt.TableName, "_", dc.ColumnName),
                                    dc,
                                    true
                                );
                        }
                    }

                    // Create an association table for every n-n relation
                    Association association=element as Association;
                    if (association!=null)
                    {
                        if (Association.IsSingleMultiplicity(association.SourceMultiplicity) || Association.IsSingleMultiplicity(association.TargetMultiplicity))
                            continue;

                        dt=ds.Tables.Add(string.Concat(association.Source.TableName, association.Target.TableName));
                        dt.ExtendedProperties.Add(typeof(DslModeling.ModelElement), association);
                        association.DataTable=dt;

                        DomainEntity keySourceEntity=association.Source;
                        //while (keySourceEntity.Parent!=null)
                        //    keySourceEntity=keySourceEntity.Parent;
                        DomainEntity keyTargetEntity=association.Target;
                        //while (keyTargetEntity.Parent!=null)
                        //    keyTargetEntity=keyTargetEntity.Parent;

                        List<DataColumn> keys=new List<DataColumn>();
                        foreach (DomainEntity associated in new DomainEntity[] { keySourceEntity, keyTargetEntity })
                        {
                            keys.AddRange(
                                CreateColumnsFromReferenceTable(
                                    dt,
                                    ds.Tables[associated.TableName],
                                    Rule.None,
                                    Rule.None,
                                    associated.Name
                                )
                            );
                        }

                        dt.Constraints.Add(
                            string.Concat("PK_", dt.TableName),
                            keys.ToArray(),
                            true
                        );
                    }
                }

                _DataSet=ds;
                if (_ElementEventsEndedEventHandler==null)
                    _ElementEventsEndedEventHandler=_InitDataSet;
                Store.EventManagerDirectory.ElementEventsEnded.Add(_ElementEventsEndedEventHandler);
            }
        }

        private IEnumerable<DataColumn> CreateColumnsFromReferenceTable(DataTable table, DataTable referenceTable, Rule deleteRule, Rule updateRule, string referenceBaseName)
        {
            List<DataColumn> columns=new List<DataColumn>();
            foreach (DataColumn column in referenceTable.PrimaryKey)
            {
                DataColumn dc=table.Columns.Add(
                    string.IsNullOrEmpty(referenceBaseName) ? column.ColumnName : string.Concat(referenceBaseName, "_", column.ColumnName),
                    column.DataType
                );
                dc.ExtendedProperties.Add(typeof(SdmlType), column.ExtendedProperties[typeof(SdmlType)]);

                columns.Add(dc);
            }

            ForeignKeyConstraint fkc=new ForeignKeyConstraint(
                string.Concat("FK_", table.TableName, "_", referenceTable.TableName),
                referenceTable.PrimaryKey,
                columns.ToArray()
            );
            fkc.DeleteRule=deleteRule;
            fkc.UpdateRule=updateRule;

            table.Constraints.Add(fkc);

            DataRelation rel=new DataRelation(
                string.Concat(table.TableName, "_", referenceTable.TableName),
                referenceTable.PrimaryKey,
                columns.ToArray()
            );
            table.DataSet.Relations.Add(rel);

            return columns;
        }

        /// <summary>Checks that entity names are not duplicated in the model.</summary>
        [DslValidation.ValidationMethod(DslValidation.ValidationCategories.Menu | DslValidation.ValidationCategories.Open | DslValidation.ValidationCategories.Save)]
        private void ValidateName(DslValidation.ValidationContext context)
        {
            Dictionary<string, IList<DslModeling.ModelElement>> entities=new Dictionary<string, IList<Microsoft.VisualStudio.Modeling.ModelElement>>(StringComparer.InvariantCultureIgnoreCase);

            foreach (DomainElement element in Elements)
            {
                DomainEntity entity=element as DomainEntity;
                if (entity==null)
                    continue;

                if (!entities.ContainsKey(entity.Name))
                    entities.Add(entity.Name, new List<DslModeling.ModelElement>(new DslModeling.ModelElement[] { entity }));
                else
                    entities[entity.Name].Add(entity);
            }

            foreach (IList<DslModeling.ModelElement> duplicates in entities.Values.Where(list => list.Count>1))
                context.LogError(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Resources.SR.EntityNameCannotBeDuplicated,
                        ((DomainEntity)duplicates[0]).Name
                    ),
                    "SDML04",
                    duplicates.ToArray()
                );
        }

        private void _InitDataSet(object sender, EventArgs e)
        {
            Store.EventManagerDirectory.ElementEventsEnded.Remove(_ElementEventsEndedEventHandler);
            _DataSet=null;
        }

        private EventHandler<DslModeling.ElementEventsEndedEventArgs> _ElementEventsEndedEventHandler;

        private DataSet _DataSet;
    }
}
